package prender;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.atomic.AtomicInteger;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspTagException;
import javax.servlet.jsp.tagext.BodyTagSupport;

import org.apache.log4j.Logger;

@SuppressWarnings("serial")
public class Slot extends BodyTagSupport {
	
	private static final Logger logger = Logger.getLogger("Slot");

	private Map<String, Object> attributes = new HashMap<String, Object>();
	void setAttribute(String name, Object value) {
		attributes.put(name,  value);
	}
	
	private String viewName;
	public void setViewName(String viewName) {
		this.viewName = viewName;
	}
	
	//*****************************************************************************************************
	// The lifecycle for webBuffer and beansToInitialize is only between doStartTag and doEndTag
	// once doEndTag returns, this object can be reused by Servlet Container
	//*****************************************************************************************************
	private WebBuffer webBuffer;
	WebBuffer getWebBuffer() {
		return webBuffer;
	}

	private List<InitializableBean> beansToInitialize;
	void addBeanForInitialization(InitializableBean beanToInitialize) {
		beansToInitialize.add(beanToInitialize);
	}

	@Override
	public int doStartTag() throws JspException {
		try {
			if (logger.isDebugEnabled()) {
				logger.debug(String.format("doStartTag: enter, this = [%x], view = [%s]", this.hashCode(), viewName));
				logger.debug(String.format("pageContext = [%s], request = [%s], response = [%s]",
					Utility.getObjectSignature(this.pageContext),
					Utility.getObjectSignature(this.pageContext.getRequest()),
					Utility.getObjectSignature(this.pageContext.getResponse())));
			}
			
			ServletRequest request = this.pageContext.getRequest();
			MyRequest myRequest = Utility.getMyRequest(request);
			if (myRequest == null) {
				// do not proceed if we didn't find signature
				// most likely Guardian is not configured or ApplicationListener is not configured
				// set to bypass mode so we do not produce any harm
				if (logger.isDebugEnabled()) {
					logger.debug("doStartTag: prender myRequest not found, bypass");
					logger.debug("doStartTag: leave");
				}
				return SKIP_BODY;
			}
			
			// so the request looks good, let's initialize first
			beansToInitialize = new LinkedList<InitializableBean>();
			attributes = new HashMap<String, Object>();
			webBuffer = null;
			
			TopContext topContext = myRequest.getTopContext();
			topContext.linkLock.lock();
			try {
				MyResponse myResponse = myRequest.getResponse();
				WebBuffer wb = myResponse.getWebBuffer();
				 
				this.pageContext.getOut().flush();   // important! Flush the JspWriter
				wb.stopPending();
				WebBuffer wb1 = wb.insertBuffer();
				WebBuffer wb2 = wb1.insertBuffer();
				myResponse.setWebBuffer(wb2);
				this.webBuffer = wb1;
				if (logger.isDebugEnabled()) {
					logger.debug(String.format("wb = [%s], wb1 = [%s], wb2 = [%s], myResponse = [%s]", 
						Utility.getObjectSignature(wb),
						Utility.getObjectSignature(wb1),
						Utility.getObjectSignature(wb2),
						Utility.getObjectSignature(myResponse)));
					logger.debug("doStartTag: leave");
				}
				return EVAL_BODY_INCLUDE;
			} finally {
				topContext.linkLock.unlock();
			}
		} catch (IOException e) {
			logger.error("IOException during Slot.doStarTag", e);
			throw new JspTagException("Slot.doStartTag", e);
		}
	}

	private void render(
		final HttpServletRequest request, 
		final HttpServletResponse response, 
		final RequestDispatcher dispatcher,
		final WebBuffer webBuffer,
		final Map<String, Object> attributes) {

		if (logger.isDebugEnabled()) {
			logger.debug("render: enter");
			logger.debug(String.format("render: request = [%s], response = [%s], dispatcher = [%s], webBuffer = [%s]", 
				Utility.getObjectSignature(request),
				Utility.getObjectSignature(response),
				Utility.getObjectSignature(dispatcher),
				Utility.getObjectSignature(webBuffer)));
		}
		
		// create a clone of the original request, so user does not change the original request
		MyRequest myRequest = Utility.getMyRequest(request);
		final TopContext topContext = myRequest.getTopContext();
		final MyRequest childRequest = new MyRequest(request);
		final MyResponse childResponse = new MyResponse(response);
		childRequest.setTopContext(topContext);
		childRequest.setResponse(childResponse);
		childResponse.setWebBuffer(webBuffer);
		for (Map.Entry<String, Object> entry : attributes.entrySet()) {
			childRequest.setAttribute(entry.getKey(), entry.getValue());
		}
		
		if (logger.isDebugEnabled()) {
			logger.debug(String.format("render: child request = [%s]", Utility.getObjectSignature(childRequest)));
			logger.debug(String.format("render: child response = [%s]", Utility.getObjectSignature(childResponse)));
		}
		
		topContext.asyncCount ++;
		ExecutorCompletionService<AsyncContext> monitor = topContext.getMonitor();
		if (logger.isDebugEnabled()) {
			logger.debug("render: slot is scheduled");
		}
		
		monitor.submit(new Callable<AsyncContext>() {
			@Override
			public AsyncContext call() {
				try {
					if (logger.isDebugEnabled()) {
						logger.debug("render: slot is picked up for rendering");
					}
					dispatcher.include(childRequest,  childResponse);
					childResponse.getWebBuffer().stopPending();
					return (new AsyncContext());
				} catch (IOException e) {
					return (new AsyncContext());
				} catch (ServletException e) {
					return (new AsyncContext());
				}
			}
		});
	}
	
	@Override
	public int doEndTag() throws JspException {
		if (logger.isDebugEnabled()) {
			logger.debug(String.format("doEndTag: enter, this = [%s]", this.hashCode()));
		}
		
		HttpServletRequest request = (HttpServletRequest)this.pageContext.getRequest();
		MyRequest myRequest = Utility.getMyRequest(request);
		if (myRequest == null) {
			if (logger.isDebugEnabled()) {
				logger.debug("doEndTag: prender myRequest not found, bypass");
				logger.debug("doEndTag: leave");
			}
			return EVAL_PAGE;
		}
		
		if (beansToInitialize.size() == 0) {
			if (logger.isDebugEnabled()) {
				logger.debug("doEndTag: no bean need to be initialized, go to render the slot");
			}
			HttpServletResponse response = (HttpServletResponse)this.pageContext.getResponse();
			RequestDispatcher dispatcher = this.pageContext.getServletContext().getRequestDispatcher(viewName);
			render(request, response, dispatcher, this.webBuffer, attributes);
		} else {
			final AtomicInteger initializedBeanCount = new AtomicInteger(0);
			for (InitializableBean bean : beansToInitialize) {
				initializeBean(bean, myRequest, initializedBeanCount);
			}
		}
		
		return EVAL_PAGE;
	}
	
	private void initializeBean(final InitializableBean bean, final MyRequest myRequest, final AtomicInteger initializedBeanCount) {
		if (logger.isDebugEnabled()) {
			logger.debug("initializeBean: enter");
			logger.debug(String.format("initializeBean: bean = [%s], myRequest = [%s]", 
				Utility.getObjectSignature(bean),
				Utility.getObjectSignature(myRequest)));
		}
		TopContext pageContext = myRequest.getTopContext();
		pageContext.asyncCount ++;
		// it is safe to reference pageContext since initializeBean is called synchronously
		final HttpServletRequest request = (HttpServletRequest)this.pageContext.getRequest();
		final HttpServletResponse response = (HttpServletResponse)this.pageContext.getResponse();
		final RequestDispatcher dispatcher = this.pageContext.getServletContext().getRequestDispatcher(viewName);
		final int beansToInitializeSize = beansToInitialize.size();
		final WebBuffer webBuffer = this.webBuffer;
		final Map<String, Object> attributes = this.attributes;
		pageContext.getMonitor().submit(new Callable<AsyncContext>() {
			@Override
			public AsyncContext call() {
				if (logger.isDebugEnabled()) {
					logger.debug("initializeBean.call: enter");
					logger.debug(String.format("initializeBean.call: bean = [%s]", Utility.getObjectSignature(bean)));
				}
				try {
					bean.initialize();
				} catch (Exception e) {
					logger.error("initializeBean.call: Failed to initialize bean", e);
				}
				
				int current = initializedBeanCount.incrementAndGet();
				if (logger.isDebugEnabled()) {
					logger.debug(String.format("initializeBean.call: completedBeanCount = %d, beansToInitialize = %d",
							current, beansToInitializeSize));
				}
				
				if (current == beansToInitializeSize) {
					if (logger.isDebugEnabled()) {
						logger.debug("initializeBean.call: all beans are initialized, go render the slot");
					}
					render(request, response, dispatcher, webBuffer, attributes);
				}
				if (logger.isDebugEnabled()) {
					logger.debug("initializeBean.call: leave");
				}
				return (new AsyncContext());
			}
		});
	}
	
	
	
}
